/*
 * ShanpshooterImpl.java   ver    31 Oct 2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.transactions.control.impl;

import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;

import org.mentalsmash.tazio.transactions.control.Snapshooter;
import org.mentalsmash.tazio.transactions.model.IllegalPropertyTypeException;
import org.mentalsmash.tazio.transactions.model.NoSuchPropertyException;
import org.mentalsmash.tazio.transactions.model.PrivateSnapshot;
import org.mentalsmash.tazio.transactions.model.RootSet;
import org.mentalsmash.tazio.transactions.model.RootSetSnapshot;
import org.mentalsmash.tazio.transactions.model.SetSnapshot;
import org.mentalsmash.tazio.transactions.model.Snapshot;
import org.mentalsmash.tazio.transactions.model.TransactionalObject;
import org.mentalsmash.tazio.transactions.model.TransactionalSet;
import org.mentalsmash.tazio.transactions.model.identifiers.OID;
import org.mentalsmash.tazio.transactions.user.TransactionalSystemRuntimeException;
import org.mentalsmash.tazio.transactions.util.Utility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;

/**
 * Simple implementation of the {@link Snapshooter} interface
 * 
 * @author Andrea Reale
 * @version 0.0.1
 * @see Snapshooter
 */
public class SnapshooterImpl implements Snapshooter {

    private final static Logger log = LoggerFactory
	    .getLogger(SnapshooterImpl.class);

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.internals.Snapshooter#createSnapshot
     * (org.mentalsmash.tazio.transactions.TransactionalObject)
     */

    private PrivateSnapshot takePrivateSnapshot(final TransactionalObject target) {
	PrivateSnapshot returnVal = null;
	synchronized (target) {
	    final OID oid = target.getObjectIdentifier();
	    final long version = target.getVersion();

	    returnVal = new PrivateSnapshot(oid, version);

	    for (final String propertyName : target.getPropertyNames()) {
		Class<?> propType;
		Serializable propValue;
		try {
		    propType = target.getPropertyType(propertyName);
		    propValue = target.getPropertyValue(propertyName);
		} catch (final NoSuchPropertyException e) {
		    log.error("UnexpectedException", e);
		    throw new TransactionalSystemRuntimeException(
			    "Unexpected Exception", e);
		}

		if (propValue == null) {
		    log.debug("Setting " + propertyName
			    + " to null for object " + oid);
		    propValue = OID.NULL_OID;
		} else if (Utility.isPrimitive(propType)) {
		    // we're ok
		} else if (Utility.implementsInterface(propValue,
			TransactionalObject.class)) {
		    // If it is a reference to another transactional object, add
		    // its OID
		    propValue = ((TransactionalObject) propValue)
			    .getObjectIdentifier();
		} else {
		    throw new IllegalPropertyTypeException(
			    "Invalid property type in object "
				    + oid
				    + ". It must be either a primitive type or an instance of TransactionalObject");
		}

		returnVal.addProperty(propertyName, propValue);
	    }

	}

	return returnVal;
    }

    @Override
    public void createSnapshot(final TransactionalObject target,
	    final Map<OID, Snapshot> destination) {
	Preconditions.checkNotNull(destination);
	/*
	 * 1. If there already is a snapshot of target in the map, do nothing
	 * (but raise a warning) 2. Take a snapshot of the target 3. For each
	 * transactional property in the target, go snapshot that property and
	 * add it to the map 4. Do this recursevly
	 */

	if (destination.containsKey(target.getObjectIdentifier())) {
	    log.warn("Sanpshot of object {} already in the map. Returning",
		    target.getObjectIdentifier());
	} else {
	    if( target instanceof RootSet) {
		final RootSet theRootSet = (RootSet)target;
		final RootSetSnapshot snap = takeRootSetSnapshot(theRootSet);
		destination.put(RootSet.ROOT_SET_OID, snap);
		
		Iterator<TransactionalObject> it = theRootSet.txIterator();
		
		while(it.hasNext()) {
		    TransactionalObject item = it.next();
		    createSnapshot(item, destination);
		}
	    }
	    else if (target instanceof TransactionalSet) {
		final TransactionalSet theSet = (TransactionalSet) target;
		final SetSnapshot snap = takeSetSnapshot(theSet);
		destination.put(theSet.getObjectIdentifier(), snap);
		// if it is a set of other TransactionalObjects snapshoot them
		if (!theSet.containsPrimitives()) {
		    for (Serializable item : theSet) {
			createSnapshot((TransactionalObject) item, destination);
		    }
		}
	    } else {

		final PrivateSnapshot snap = takePrivateSnapshot(target);
		destination.put(target.getObjectIdentifier(), snap);

		// recursively take snapshot of referenced properties
		for (final String propertyName : target.getPropertyNames()) {
		    Serializable property = null;

		    try {
			property = target.getPropertyValue(propertyName);
		    } catch (final NoSuchPropertyException e) {
			log.error("Fatal error: this property should really"
				+ " be there, since it was checked");
			throw new TransactionalSystemRuntimeException(
				"Fatal error while accessing property "
					+ propertyName + " in object "
					+ target.getObjectIdentifier());
		    }
		    if (property instanceof TransactionalObject) {
			createSnapshot((TransactionalObject) property,
				destination);
		    }
		}
	    }

	}

	return;
    }

    /**
     * @param theRootSet
     * @return
     */
    private RootSetSnapshot takeRootSetSnapshot(RootSet theRootSet) {
	Preconditions.checkNotNull(theRootSet);
	
	final RootSetSnapshot toReturn = new RootSetSnapshot(theRootSet.getVersion());
	
	synchronized (theRootSet) {
	    Iterator<TransactionalObject> iterator = theRootSet.txIterator();
	    while(iterator.hasNext()) {
		TransactionalObject item = iterator.next();
		toReturn.add(item);
	    }
	}
	
	return toReturn;
	
    }

    private SetSnapshot takeSetSnapshot(final TransactionalSet theSet) {
	Preconditions.checkNotNull(theSet);

	final SetSnapshot toReturn = new SetSnapshot(theSet
		.getObjectIdentifier(), theSet.getVersion());

	synchronized (theSet) {
	    if (theSet.containsPrimitives()) {
		toReturn.addAll(theSet);
	    } else { // theSet is assured to contain only instances of
		// TransactionalObjects
		for (Serializable item : theSet) {
		    final TransactionalObject txObj = (TransactionalObject) item;
		    toReturn.add(txObj.getObjectIdentifier());
		}
	    }
	}

	return toReturn;
    }
}
